Passed
Push — master ( acbb55...c12e14 )
by Rafael S.
02:32
created

index.js ➔ truncateSamples_   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 4
dl 0
loc 11
rs 10
nop 1
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from 'bitdepth';
33
import * as imaadpcm from 'imaadpcm';
34
import * as alawmulaw from 'alawmulaw';
35
import {encode, decode} from 'base64-arraybuffer-es6';
36
import {unpackArray, packArrayTo, unpackArrayTo,
37
  unpack, packTo} from 'byte-data';
38
39
import makeWavHeader from './lib/make-wav-header.js';
40
import validateWavHeader from './lib/validate-wav-header';
41
import writeWavBuffer from './lib/wav-buffer-writer.js';
42
import readWavBuffer from './lib/wav-buffer-reader.js';
43
import WavBuffer from './lib/wav-buffer.js';
44
45
/**
46
 * Class representing a wav file.
47
 * @extends WavBuffer
48
 * @ignore
49
 */
50
export default class WaveFile extends WavBuffer {
51
52
  /**
53
   * @param {?Uint8Array} bytes A wave file buffer.
54
   * @throws {Error} If no 'RIFF' chunk is found.
55
   * @throws {Error} If no 'fmt ' chunk is found.
56
   * @throws {Error} If no 'data' chunk is found.
57
   */
58
  constructor(bytes=null) {
59
    super();
60
    /**
61
     * The bit depth code according to the samples.
62
     * @type {string}
63
     */
64
    this.bitDepth = '0';
65
    /**
66
     * @type {!Object}
67
     * @private
68
     */
69
    this.dataType = {};
70
    // Load a file from the buffer if one was passed
71
    // when creating the object
72
    if (bytes) {
73
      this.fromBuffer(bytes);
74
    }
75
  }
76
77
  /**
78
   * Return the sample at a given index.
79
   * @param {number} index The sample index.
80
   * @return {number} The sample.
81
   * @throws {Error} If the sample index is off range.
82
   */
83
  getSample(index) {
84
    index = index * (this.dataType.bits / 8);
85
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
86
      throw new Error('Range error');
87
    }
88
    return unpack(
89
      this.data.samples.slice(index, index + this.dataType.bits / 8),
90
      this.dataType);
91
  }
92
93
  /**
94
   * Set the sample at a given index.
95
   * @param {number} index The sample index.
96
   * @param {number} sample The sample.
97
   * @throws {Error} If the sample index is off range.
98
   */
99
  setSample(index, sample) {
100
    index = index * (this.dataType.bits / 8);
101
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
102
      throw new Error('Range error');
103
    }
104
    packTo(sample, this.dataType, this.data.samples, index);
105
  }
106
107
  /**
108
   * Set up the WaveFile object based on the arguments passed.
109
   * @param {number} numChannels The number of channels
110
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
111
   * @param {number} sampleRate The sample rate.
112
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
113
   * @param {string} bitDepthCode The audio bit depth code.
114
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
115
   *    or any value between '8' and '32' (like '12').
116
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
117
   *    The samples. Must be in the correct range according to the bit depth.
118
   * @param {?Object} options Optional. Used to force the container
119
   *    as RIFX with {'container': 'RIFX'}
120
   * @throws {Error} If any argument does not meet the criteria.
121
   */
122
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
123
    if (!options.container) {
124
      options.container = 'RIFF';
125
    }
126
    this.container = options.container;
127
    this.bitDepth = bitDepthCode;
128
    samples = this.interleave_(samples);
129
    this.updateDataType_();
130
    /** @type {number} */
131
    let numBytes = this.dataType.bits / 8;
132
    this.data.samples = new Uint8Array(samples.length * numBytes);
133
    packArrayTo(samples, this.dataType, this.data.samples);
134
    /** @type {!Object} */
135
    let header = makeWavHeader(
136
      bitDepthCode, numChannels, sampleRate,
137
      numBytes, this.data.samples.length, options);
138
    this.clearHeader_();
139
    this.chunkSize = header.chunkSize;
140
    this.format = header.format;
141
    this.fmt = header.fmt;
142
    if (header.fact) {
143
      this.fact = header.fact;
144
    }
145
    this.data.chunkId = 'data';
146
    this.data.chunkSize = this.data.samples.length;
147
    validateWavHeader(this);
148
  }
149
150
  /**
151
   * Set up the WaveFile object from a byte buffer.
152
   * @param {!Uint8Array} bytes The buffer.
153
   * @param {boolean=} samples True if the samples should be loaded.
154
   * @throws {Error} If container is not RIFF, RIFX or RF64.
155
   * @throws {Error} If no 'fmt ' chunk is found.
156
   * @throws {Error} If no 'data' chunk is found.
157
   */
158
  fromBuffer(bytes, samples=true) {
159
    this.clearHeader_();
160
    readWavBuffer(bytes, samples, this);
161
    this.bitDepthFromFmt_();
162
    this.updateDataType_();
163
  }
164
165
  /**
166
   * Return a byte buffer representig the WaveFile object as a .wav file.
167
   * The return value of this method can be written straight to disk.
168
   * @return {!Uint8Array} A .wav file.
169
   * @throws {Error} If any property of the object appears invalid.
170
   */
171
  toBuffer() {
172
    validateWavHeader(this);
173
    return writeWavBuffer(this);
174
  }
175
176
  /**
177
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
178
   * @param {string} base64String A .wav file as a base64 string.
179
   * @throws {Error} If any property of the object appears invalid.
180
   */
181
  fromBase64(base64String) {
182
    this.fromBuffer(new Uint8Array(decode(base64String)));
183
  }
184
185
  /**
186
   * Return a base64 string representig the WaveFile object as a .wav file.
187
   * @return {string} A .wav file as a base64 string.
188
   * @throws {Error} If any property of the object appears invalid.
189
   */
190
  toBase64() {
191
    /** @type {!Uint8Array} */
192
    let buffer = this.toBuffer();
193
    return encode(buffer, 0, buffer.length);
194
  }
195
196
  /**
197
   * Return a DataURI string representig the WaveFile object as a .wav file.
198
   * The return of this method can be used to load the audio in browsers.
199
   * @return {string} A .wav file as a DataURI.
200
   * @throws {Error} If any property of the object appears invalid.
201
   */
202
  toDataURI() {
203
    return 'data:audio/wav;base64,' + this.toBase64();
204
  }
205
206
  /**
207
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
208
   * @param {string} dataURI A .wav file as DataURI.
209
   * @throws {Error} If any property of the object appears invalid.
210
   */
211
  fromDataURI(dataURI) {
212
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
213
  }
214
215
  /**
216
   * Force a file as RIFF.
217
   */
218
  toRIFF() {
219
    this.fromScratch(
220
      this.fmt.numChannels,
221
      this.fmt.sampleRate,
222
      this.bitDepth,
223
      unpackArray(this.data.samples, this.dataType));
224
  }
225
226
  /**
227
   * Force a file as RIFX.
228
   */
229
  toRIFX() {
230
    this.fromScratch(
231
      this.fmt.numChannels,
232
      this.fmt.sampleRate,
233
      this.bitDepth,
234
      unpackArray(this.data.samples, this.dataType),
235
      {container: 'RIFX'});
236
  }
237
238
  /**
239
   * Change the bit depth of the samples.
240
   * @param {string} newBitDepth The new bit depth of the samples.
241
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
242
   * @param {boolean} changeResolution A boolean indicating if the
243
   *    resolution of samples should be actually changed or not.
244
   * @throws {Error} If the bit depth is not valid.
245
   */
246
  toBitDepth(newBitDepth, changeResolution=true) {
247
    /** @type {string} */
248
    let toBitDepth = newBitDepth;
249
    /** @type {string} */
250
    let thisBitDepth = this.bitDepth;
251
    if (!changeResolution) {
252
      if (newBitDepth != '32f') {
253
        toBitDepth = this.dataType.bits.toString();
254
      }
255
      thisBitDepth = this.dataType.bits;
256
    }
257
    this.assureUncompressed_();
258
    /** @type {number} */
259
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
260
    /** @type {!Float64Array} */
261
    let typedSamplesInput = new Float64Array(sampleCount + 1);
262
    /** @type {!Float64Array} */
263
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
264
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
265
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
266
      this.truncateSamples_(typedSamplesInput);
267
    }
268
    bitDepthLib(
269
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
270
    this.fromScratch(
271
      this.fmt.numChannels,
272
      this.fmt.sampleRate,
273
      newBitDepth,
274
      typedSamplesOutput,
275
      {container: this.correctContainer_()});
276
  }
277
278
  /**
279
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
280
   * @throws {Error} If sample rate is not 8000.
281
   * @throws {Error} If number of channels is not 1.
282
   */
283
  toIMAADPCM() {
284
    if (this.fmt.sampleRate !== 8000) {
285
      throw new Error(
286
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
287
    } else if (this.fmt.numChannels !== 1) {
288
      throw new Error(
289
        'Only mono files can be compressed as IMA-ADPCM.');
290
    } else {
291
      this.assure16Bit_();
292
      let output = new Int16Array(this.data.samples.length / 2);
293
      unpackArrayTo(this.data.samples, this.dataType, output);
294
      this.fromScratch(
295
        this.fmt.numChannels,
296
        this.fmt.sampleRate,
297
        '4',
298
        imaadpcm.encode(output),
299
        {container: this.correctContainer_()});
300
    }
301
  }
302
303
  /**
304
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
305
   * @param {string} bitDepthCode The new bit depth of the samples.
306
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
307
   *    Optional. Default is 16.
308
   */
309
  fromIMAADPCM(bitDepthCode='16') {
310
    this.fromScratch(
311
      this.fmt.numChannels,
312
      this.fmt.sampleRate,
313
      '16',
314
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
315
      {container: this.correctContainer_()});
316
    if (bitDepthCode != '16') {
317
      this.toBitDepth(bitDepthCode);
318
    }
319
  }
320
321
  /**
322
   * Encode a 16-bit wave file as 8-bit A-Law.
323
   */
324
  toALaw() {
325
    this.assure16Bit_();
326
    let output = new Int16Array(this.data.samples.length / 2);
327
    unpackArrayTo(this.data.samples, this.dataType, output);
328
    this.fromScratch(
329
      this.fmt.numChannels,
330
      this.fmt.sampleRate,
331
      '8a',
332
      alawmulaw.alaw.encode(output),
333
      {container: this.correctContainer_()});
334
  }
335
336
  /**
337
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
338
   * @param {string} bitDepthCode The new bit depth of the samples.
339
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
340
   *    Optional. Default is 16.
341
   */
342
  fromALaw(bitDepthCode='16') {
343
    this.fromScratch(
344
      this.fmt.numChannels,
345
      this.fmt.sampleRate,
346
      '16',
347
      alawmulaw.alaw.decode(this.data.samples),
348
      {container: this.correctContainer_()});
349
    if (bitDepthCode != '16') {
350
      this.toBitDepth(bitDepthCode);
351
    }
352
  }
353
354
  /**
355
   * Encode 16-bit wave file as 8-bit mu-Law.
356
   */
357
  toMuLaw() {
358
    this.assure16Bit_();
359
    let output = new Int16Array(this.data.samples.length / 2);
360
    unpackArrayTo(this.data.samples, this.dataType, output);
361
    this.fromScratch(
362
      this.fmt.numChannels,
363
      this.fmt.sampleRate,
364
      '8m',
365
      alawmulaw.mulaw.encode(output),
366
      {container: this.correctContainer_()});
367
  }
368
369
  /**
370
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
371
   * @param {string} bitDepthCode The new bit depth of the samples.
372
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
373
   *    Optional. Default is 16.
374
   */
375
  fromMuLaw(bitDepthCode='16') {
376
    this.fromScratch(
377
      this.fmt.numChannels,
378
      this.fmt.sampleRate,
379
      '16',
380
      alawmulaw.mulaw.decode(this.data.samples),
381
      {container: this.correctContainer_()});
382
    if (bitDepthCode != '16') {
383
      this.toBitDepth(bitDepthCode);
384
    }
385
  }
386
387
  /**
388
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
389
   * then it is created. It if exists, it is overwritten.
390
   * @param {string} tag The tag name.
391
   * @param {string} value The tag value.
392
   * @throws {Error} If the tag name is not valid.
393
   */
394
  setTag(tag, value) {
395
    tag = this.fixTagName_(tag);
396
    /** @type {!Object} */
397
    let index = this.getTagIndex_(tag);
398
    if (index.TAG !== null) {
399
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
400
        value.length + 1;
401
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
402
    } else if (index.LIST !== null) {
403
      this.LIST[index.LIST].subChunks.push({
404
        chunkId: tag,
405
        chunkSize: value.length + 1,
406
        value: value});
407
    } else {
408
      this.LIST.push({
409
        chunkId: 'LIST',
410
        chunkSize: 8 + value.length + 1,
411
        format: 'INFO',
412
        subChunks: []});
413
      this.LIST[this.LIST.length - 1].subChunks.push({
414
        chunkId: tag,
415
        chunkSize: value.length + 1,
416
        value: value});
417
    }
418
  }
419
420
  /**
421
   * Return the value of a RIFF tag in the INFO chunk.
422
   * @param {string} tag The tag name.
423
   * @return {?string} The value if the tag is found, null otherwise.
424
   */
425
  getTag(tag) {
426
    /** @type {!Object} */
427
    let index = this.getTagIndex_(tag);
428
    if (index.TAG !== null) {
429
      return this.LIST[index.LIST].subChunks[index.TAG].value;
430
    }
431
    return null;
432
  }
433
434
  /**
435
   * Return a Object<tag, value> with the RIFF tags in the file.
436
   * @return {!Object<string, string>} The file tags.
437
   */
438
  listTags() {
439
    /** @type {?number} */
440
    let index = this.getLISTINFOIndex_();
441
    /** @type {!Object} */
442
    let tags = {};
443
    if (index !== null) {
444
      for (let i=0; i<this.LIST[index].subChunks.length; i++) {
445
        tags[this.LIST[index].subChunks[i].chunkId] =
446
          this.LIST[index].subChunks[i].value;
447
      }
448
    }
449
    return tags;
450
  }
451
452
  /**
453
   * Remove a RIFF tag in the INFO chunk.
454
   * @param {string} tag The tag name.
455
   * @return {boolean} True if a tag was deleted.
456
   */
457
  deleteTag(tag) {
458
    /** @type {!Object} */
459
    let index = this.getTagIndex_(tag);
460
    if (index.TAG !== null) {
461
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
462
      return true;
463
    }
464
    return false;
465
  }
466
467
  /**
468
   * Create a cue point in the wave file.
469
   * @param {number} position The cue point position in milliseconds.
470
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
471
   */
472
  setCuePoint(position, labl='') {
473
    this.cue.chunkId = 'cue ';
474
    position = (position * this.fmt.sampleRate) / 1000;
475
    /** @type {!Array<!Object>} */
476
    let existingPoints = this.getCuePoints_();
477
    this.clearLISTadtl_();
478
    /** @type {number} */
479
    let len = this.cue.points.length;
480
    this.cue.points = [];
481
    /** @type {boolean} */
482
    let hasSet = false;
483
    if (len === 0) {
484
      this.setCuePoint_(position, 1, labl);
485
    } else {
486
      for (let i=0; i<len; i++) {
487
        if (existingPoints[i].dwPosition > position && !hasSet) {
488
          this.setCuePoint_(position, i + 1, labl);
489
          this.setCuePoint_(
490
            existingPoints[i].dwPosition,
491
            i + 2,
492
            existingPoints[i].label);
493
          hasSet = true;
494
        } else {
495
          this.setCuePoint_(
496
            existingPoints[i].dwPosition,
497
            i + 1,
498
            existingPoints[i].label);
499
        }
500
      }
501
      if (!hasSet) {
502
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
503
      }
504
    }
505
    this.cue.dwCuePoints = this.cue.points.length;
506
  }
507
508
  /**
509
   * Remove a cue point from a wave file.
510
   * @param {number} index the index of the point. First is 1,
511
   *    second is 2, and so on.
512
   */
513
  deleteCuePoint(index) {
514
    this.cue.chunkId = 'cue ';
515
    /** @type {!Array<!Object>} */
516
    let existingPoints = this.getCuePoints_();
517
    this.clearLISTadtl_();
518
    /** @type {number} */
519
    let len = this.cue.points.length;
520
    this.cue.points = [];
521
    for (let i=0; i<len; i++) {
522
      if (i + 1 !== index) {
523
        this.setCuePoint_(
524
          existingPoints[i].dwPosition,
525
          i + 1,
526
          existingPoints[i].label);
527
      }
528
    }
529
    this.cue.dwCuePoints = this.cue.points.length;
530
    if (this.cue.dwCuePoints) {
531
      this.cue.chunkId = 'cue ';
532
    } else {
533
      this.cue.chunkId = '';
534
      this.clearLISTadtl_();
535
    }
536
  }
537
538
  /**
539
   * Return an array with all cue points in the file, in the order they appear
540
   * in the file.
541
   * The difference between this method and using the list in WaveFile.cue
542
   * is that the return value of this method includes the position in
543
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
544
   * @return {!Array<!Object>}
545
   */
546
  listCuePoints() {
547
    /** @type {!Array<!Object>} */
548
    let points = this.getCuePoints_();
549
    for (let i=0; i<points.length; i++) {
550
      points[i].milliseconds =
551
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
552
    }
553
    return points;
554
  }
555
556
  /**
557
   * Update the label of a cue point.
558
   * @param {number} pointIndex The ID of the cue point.
559
   * @param {string} label The new text for the label.
560
   */
561
  updateLabel(pointIndex, label) {
562
    /** @type {?number} */
563
    let adtlIndex = this.getAdtlChunk_();
564
    if (adtlIndex !== null) {
565
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
566
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
567
            pointIndex) {
568
          this.LIST[adtlIndex].subChunks[i].value = label;
569
        }
570
      }
571
    }
572
  }
573
574
  /**
575
   * Set the string code of the bit depth based on the 'fmt ' chunk.
576
   * @private
577
   */
578
  bitDepthFromFmt_() {
579
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
580
      this.bitDepth = '32f';
581
    } else if (this.fmt.audioFormat === 6) {
582
      this.bitDepth = '8a';
583
    } else if (this.fmt.audioFormat === 7) {
584
      this.bitDepth = '8m';
585
    } else {
586
      this.bitDepth = this.fmt.bitsPerSample.toString();
587
    }
588
  }
589
  
590
  /**
591
   * Push a new cue point in this.cue.points.
592
   * @param {number} position The position in milliseconds.
593
   * @param {number} dwName the dwName of the cue point
594
   * @private
595
   */
596
  setCuePoint_(position, dwName, label) {
597
    this.cue.points.push({
598
      dwName: dwName,
599
      dwPosition: position,
600
      fccChunk: 'data',
601
      dwChunkStart: 0,
602
      dwBlockStart: 0,
603
      dwSampleOffset: position,
604
    });
605
    this.setLabl_(dwName, label);
606
  }
607
608
  /**
609
   * Return an array with all cue points in the file, in the order they appear
610
   * in the file.
611
   * @return {!Array<!Object>}
612
   * @private
613
   */
614
  getCuePoints_() {
615
    /** @type {!Array<!Object>} */
616
    let points = [];
617
    for (let i=0; i<this.cue.points.length; i++) {
618
      points.push({
619
        dwPosition: this.cue.points[i].dwPosition,
620
        label: this.getLabelForCuePoint_(
621
          this.cue.points[i].dwName)});
622
    }
623
    return points;
624
  }
625
626
  /**
627
   * Return the label of a cue point.
628
   * @param {number} pointDwName The ID of the cue point.
629
   * @return {string}
630
   * @private
631
   */
632
  getLabelForCuePoint_(pointDwName) {
633
    /** @type {?number} */
634
    let adtlIndex = this.getAdtlChunk_();
635
    if (adtlIndex !== null) {
636
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
637
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
638
            pointDwName) {
639
          return this.LIST[adtlIndex].subChunks[i].value;
640
        }
641
      }
642
    }
643
    return '';
644
  }
645
646
  /**
647
   * Clear any LIST chunk labeled as 'adtl'.
648
   * @private
649
   */
650
  clearLISTadtl_() {
651
    for (let i=0; i<this.LIST.length; i++) {
652
      if (this.LIST[i].format == 'adtl') {
653
        this.LIST.splice(i);
654
      }
655
    }
656
  }
657
658
  /**
659
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
660
   * @param {number} dwName The ID of the cue point.
661
   * @param {string} label The label for the cue point.
662
   * @private
663
   */
664
  setLabl_(dwName, label) {
665
    /** @type {?number} */
666
    let adtlIndex = this.getAdtlChunk_();
667
    if (adtlIndex === null) {
668
      this.LIST.push({
669
        chunkId: 'LIST',
670
        chunkSize: 4,
671
        format: 'adtl',
672
        subChunks: []});
673
      adtlIndex = this.LIST.length - 1;
674
    }
675
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
676
  }
677
678
  /**
679
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
680
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
681
   * @param {number} dwName The ID of the cue point.
682
   * @param {string} label The label for the cue point.
683
   * @private
684
   */
685
  setLabelText_(adtlIndex, dwName, label) {
686
    this.LIST[adtlIndex].subChunks.push({
687
      chunkId: 'labl',
688
      chunkSize: label.length,
689
      dwName: dwName,
690
      value: label
691
    });
692
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
693
  }
694
695
  /**
696
   * Return the index of the 'adtl' LIST in this.LIST.
697
   * @return {?number}
698
   * @private
699
   */
700
  getAdtlChunk_() {
701
    for (let i=0; i<this.LIST.length; i++) {
702
      if (this.LIST[i].format == 'adtl') {
703
        return i;
704
      }
705
    }
706
    return null;
707
  }
708
709
  /**
710
   * Return the index of the INFO chunk in the LIST chunk.
711
   * @return {?number} the index of the INFO chunk.
712
   * @private
713
   */
714
  getLISTINFOIndex_() {
715
    /** @type {?number} */
716
    let index = null;
717
    for (let i=0; i<this.LIST.length; i++) {
718
      if (this.LIST[i].format === 'INFO') {
719
        index = i;
720
        break;
721
      }
722
    }
723
    return index;
724
  }
725
726
  /**
727
   * Return the index of a tag in a FILE chunk.
728
   * @param {string} tag The tag name.
729
   * @return {!Object<string, ?number>}
730
   *    Object.LIST is the INFO index in LIST
731
   *    Object.TAG is the tag index in the INFO
732
   * @private
733
   */
734
  getTagIndex_(tag) {
735
    /** @type {!Object<string, ?number>} */
736
    let index = {LIST: null, TAG: null};
737
    for (let i=0; i<this.LIST.length; i++) {
738
      if (this.LIST[i].format == 'INFO') {
739
        index.LIST = i;
740
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
741
          if (this.LIST[i].subChunks[j].chunkId == tag) {
742
            index.TAG = j;
743
            break;
744
          }
745
        }
746
        break;
747
      }
748
    }
749
    return index;
750
  }
751
752
  /**
753
   * Fix a RIFF tag format if possible, throw an error otherwise.
754
   * @param {string} tag The tag name.
755
   * @return {string} The tag name in proper fourCC format.
756
   * @private
757
   */
758
  fixTagName_(tag) {
759
    if (tag.constructor !== String) {
760
      throw new Error('Invalid tag name.');
761
    } else if (tag.length < 4) {
762
      for (let i=0; i<4-tag.length; i++) {
763
        tag += ' ';
764
      }
765
    }
766
    return tag;
767
  }
768
769
  /**
770
   * Reset attributes that should emptied when a file is
771
   * created with the fromScratch() or fromBuffer() methods.
772
   * @private
773
   */
774
  clearHeader_() {
775
    this.fmt.cbSize = 0;
776
    this.fmt.validBitsPerSample = 0;
777
    this.fact.chunkId = '';
778
    this.ds64.chunkId = '';
779
  }
780
781
  /**
782
   * Make the file 16-bit if it is not.
783
   * @private
784
   */
785
  assure16Bit_() {
786
    this.assureUncompressed_();
787
    if (this.bitDepth != '16') {
788
      this.toBitDepth('16');
789
    }
790
  }
791
792
  /**
793
   * Uncompress the samples in case of a compressed file.
794
   * @private
795
   */
796
  assureUncompressed_() {
797
    if (this.bitDepth == '8a') {
798
      this.fromALaw();
799
    } else if (this.bitDepth == '8m') {
800
      this.fromMuLaw();
801
    } else if (this.bitDepth == '4') {
802
      this.fromIMAADPCM();
803
    }
804
  }
805
806
  /**
807
   * Set up the WaveFile object from a byte buffer.
808
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
809
   * @private
810
   */
811
  interleave_(samples) {
812
    if (samples.length > 0) {
813
      if (samples[0].constructor === Array) {
814
        /** @type {!Array<number>} */
815
        let finalSamples = [];
816
        for (let i=0; i < samples[0].length; i++) {
817
          for (let j=0; j < samples.length; j++) {
818
            finalSamples.push(samples[j][i]);
819
          }
820
        }
821
        samples = finalSamples;
822
      }
823
    }
824
    return samples;
825
  }
826
827
  /**
828
   * Update the type definition used to read and write the samples.
829
   * @private
830
   */
831
  updateDataType_() {
832
    /** @type {!Object} */
833
    this.dataType = {
834
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
835
      float: this.bitDepth == '32f' || this.bitDepth == '64',
836
      signed: this.bitDepth != '8',
837
      be: this.container == 'RIFX'
838
    };
839
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
840
      this.dataType.bits = 8;
841
      this.dataType.signed = false;
842
    }
843
  }
844
845
  /**
846
   * Return 'RIFF' if the container is 'RF64', the current container name
847
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
848
   * @return {string}
849
   * @private
850
   */
851
  correctContainer_() {
852
    return this.container == 'RF64' ? 'RIFF' : this.container;
853
  }
854
855
  /**
856
   * Truncate float samples on over and underflow.
857
   * @private
858
   */
859
  truncateSamples_(samples) {
860
    /** @type {number} */   
861
    let len = samples.length;
862
    for (let i=0; i<len; i++) {
863
      if (samples[i] > 1) {
864
        samples[i] = 1;
865
      } else if (samples[i] < -1) {
866
        samples[i] = -1;
867
      }
868
    }
869
  }
870
}
871